Keras(TensorFlow)のImageDataGeneratorをカスタマイズする
こんちには。
データアナリティクス事業本部機械学習チームの中村です。
今回は、画像データの水増し(Data Augmentation)が可能な、KerasのImageDataGeneratorのカスタマイズ方法について紹介します。
ImageDataGeneratorの基本的な使い方は、以下の前回記事を参照ください。
カスタマイズの概要
カスタマイズの概要は以下です。
- ImageDataGeneratorを継承するクラスを作成する。
- flowという関数を修正する。
- whileループ内でyieldで返す関数として実装する。
- whileループ内ではカスタムでいれたい変換処理を実装する。
今回はこの方法でrandom erasingという手法を導入してみます。
random erasingについて
random erasingは画像の一部をマスクすることで、特定部位の特徴に依存しないようにする手法です。
オクルージョン自動で生成し、汎化性能を上げる工夫という風にも捉えられます。
パラメータとしては、以下を準備する必要があります。
- random erasingを発生させる確率
- マスクする面積の範囲(相対値で表現)
- アスペクト比の範囲
詳細は以下の元論文を参照ください。
変換の実装
前準備
モジュールのインポートやデータ取得を行います。
import tensorflow as tf import tensorflow.keras as keras import numpy as np import matplotlib.pyplot as plt
前回と同じく、データはCIFAR-10というRGB画像のデータセットを使います。
dataset = keras.datasets.cifar10 (train_images, train_labels), (test_images, test_labels) = dataset.load_data()
前回同様、一つの画像を複製します。
sample_index = 7 train_image_sample = train_images[sample_index] train_label_sample = train_labels[sample_index]
この画像を描画しておきます。(いつものお馬さんですね)
plt.figure(figsize=(3,3)) plt.imshow(train_sample) plt.tick_params(labelbottom='off') plt.tick_params(labelleft='off')
カスタムクラス作成
以下のようにImageDataGenerator
を継承したカスタムクラスを作成します。
class MyImageDataGenerator(keras.preprocessing.image.ImageDataGenerator): def __init__(self, random_erasing_probability = None, random_erasing_area_ratio = [0.02, 0.4], random_erasing_aspect_ratio = [0.3, 1/0.3], random_erasing_mask_value = [0, 1], *args, **kwargs ): super().__init__(*args, **kwargs) self.random_erasing_probability = random_erasing_probability self.random_erasing_area_ratio = random_erasing_area_ratio self.random_erasing_aspect_ratio = random_erasing_aspect_ratio self.random_erasing_mask_value = random_erasing_mask_value def random_erasing(self, X, erasing_probability, erasing_area_ratio_range, erasing_aspect_ratio_range, random_erasing_mask_value ): # ...一旦省略... def flow(self, seed=None, *args, **kwargs): batch_gen = super().flow(seed=seed, *args, **kwargs) while True: batch_x, batch_y = next(batch_gen) # random erasing if self.random_erasing_probability is not None: batch_x = self.random_erasing( batch_x, self.random_erasing_probability, self.random_erasing_area_ratio, self.random_erasing_aspect_ratio, self.random_erasing_mask_value) yield (batch_x, batch_y)
__init__
でカスタム処理で使うパラメータを引数に追加します。各パラメータは以下の通りです。
random_erasing_probability
: random erasingを発生させる確率random_erasing_area_ratio
: マスクする面積の範囲(相対値で表現)random_erasing_aspect_ratio
: アスペクト比の範囲random_erasing_mask_value
: マスクする値の取りうる範囲
random_erasing_mask_value
は、元論文に記載がありませんが、画像の画素値の正規化を
どこで行うかによって、設定を変更する場合が都合が良い場合があるため、準備しています。
flow
の部分も以下のように変更しています。
- 継承元の
flow
を実行してgeneratorを取得します。 - whileループ内では、まずgeneratorからバッチデータを取得します。
- そして入力データ
batch_x
をrandom_erasing関数で処理したものに上書きし、ラベルbatch_y
とともにyieldで返します。
random erasing処理は別途関数として以下のように定義します。
(関数化しておいた方が複数のデータ拡張をカスタマイズする時に全体の見通しがよさそうです。)
def random_erasing(self, X, erasing_probability, erasing_area_ratio_range, erasing_aspect_ratio_range, random_erasing_mask_value ): X_copy = X.copy() batch_size, H, W, C = X_copy.shape original_area = H * W for batch_index in range(batch_size): if erasing_probability < np.random.rand(): continue # はみ出さないようリトライするループ while True: # マスクする面積をサンプリング erasing_area = np.random.uniform( erasing_area_ratio_range[0], erasing_area_ratio_range[1] ) * original_area # マスクするアスペクト比をサンプリング erasing_aspect_ratio = np.random.uniform( erasing_aspect_ratio_range[0], erasing_aspect_ratio_range[1] ) # 面積とアスペクト比から高さと幅を計算 erasing_height = int(np.sqrt(erasing_area * erasing_aspect_ratio)) erasing_width = int(np.sqrt(erasing_area / erasing_aspect_ratio)) # マスクを配置する端点をサンプリング erasing_left_top_x = np.random.randint(0, W) erasing_left_top_y = np.random.randint(0, H) # マスクが元画像をはみ出すかどうかを計算 if erasing_left_top_x + erasing_width <= W \ and erasing_left_top_y + erasing_height <= H: break # マスクする値の生成 erasing_values = np.random.uniform( random_erasing_mask_value[0], random_erasing_mask_value[1], (erasing_height, erasing_width, C) ) X_copy[batch_index, erasing_left_top_y:erasing_left_top_y + erasing_height, erasing_left_top_x:erasing_left_top_x + erasing_width, :] = erasing_values return X_copy
random erasingの処理自体は、工夫することでforループをなくす形式でもっと高速にできたり、マスクがはみ出す場合の処理を効率的にする余地がありますが、今回はこの実装で動かしてみます。
描画用関数の準備
こちらは前回とほとんど同じ処理です。一部、MyImageDataGenerator
用に少し改変しています。
def plot_augmentation_image(train_image_sample, train_label_sample, params): # 同じデータを16個複製する train_image_samples = np.repeat( train_image_sample.reshape((1, *train_image_sample.shape)), 16, axis=0) train_label_sample = np.repeat( train_label_sample.reshape((1, *train_label_sample.shape)), 16, axis=0) # 16個に対してparamsで与えられた変換を実施 data_generator = MyImageDataGenerator(**params) generator = data_generator.flow( x=train_image_samples, y=train_label_sample, batch_size=16) # 変換後のデータを取得 batch_x, batch_y = generator.__next__() # 変換後はfloat32となっているため、uint8に変換 batch_x = batch_x.astype(np.uint8) # 描画処理 plt.figure(figsize=(10,10)) for i in range(16): plt.subplot(4,4,i+1) plt.imshow(batch_x[i]) plt.tick_params(labelbottom='off') plt.tick_params(labelleft='off')
描画して動作確認
まずは元論文でベースラインとして設定されているパラメータで検証します。
params = { 'random_erasing_probability': 0.5, 'random_erasing_area_ratio': [0.02, 0.4], 'random_erasing_aspect_ratio': [0.3, 1/0.3], 'random_erasing_mask_value': [0, 255], } plot_augmentation_image(train_image_sample, train_label_sample, params)
random_erasing_probability
の設定どおり、約半数の画像がマスクされています。
マスク値はランダムで埋めており、元論文でも平均値や固定値等いろいろ試行錯誤されているものの、ランダムをベースラインとして使用しているようです。
例えば、ramdon_erasing_probability
を100%にし、領域は元画像の半分まで、アスペクト比は正方形にしてみます。
params = { 'random_erasing_probability': 1.0, 'random_erasing_area_ratio': [0.5, 0.5], 'random_erasing_aspect_ratio': [1.0, 1.0], 'random_erasing_mask_value': [0, 255], } plot_augmentation_image(train_image_sample, train_label_sample, params)
設定どおりに変更されていることが確認できます。
トレーニング
前回同様、サンプルモデルで学習してみます。
まずはgeneratorを作成します。
params = { 'random_erasing_probability': 0.5, 'random_erasing_area_ratio': [0.02, 0.4], 'random_erasing_aspect_ratio': [0.3, 1/0.3], 'random_erasing_mask_value': [0, 255], } data_generator = MyImageDataGenerator(**params) batch_size = 32 generator = data_generator.flow(x=train_images, y=train_labels, batch_size=32)
モデル定義は前回と同様です。
model = keras.Sequential([ keras.layers.Conv2D(32, (3, 3), input_shape=(32, 32, 3), padding='same'), keras.layers.ReLU(), keras.layers.MaxPooling2D((2, 2)), keras.layers.Conv2D(32, (3, 3), padding='same'), keras.layers.ReLU(), keras.layers.MaxPooling2D((2, 2)), keras.layers.GlobalAveragePooling2D(), keras.layers.Dense(10, activation='softmax'), ])
学習実行時のみ、fit
関数にgeneratorを渡すことになるため注意が必要です。
model.compile( optimizer='Adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) model.fit(generator, epochs=3, steps_per_epoch=len(train_images) // batch_size)
カスタマイズしない場合は、fit
関数にiteratorを渡していましたが、今回のカスタマイズによりgeneratorを渡す形となっているため、step_per_epoch
で、エポックの終わりを明示的に指定する必要があります。
まとめ
いかがでしたでしょうか?画像のデータ拡張手法は、色々と新しい手法が開発されますので、カスタマイズを自身で実装する場合もあるかと思います。そんな時に本記事がお役に立てば幸いです。
データ拡張には複数画像を使うケースなどもありますので、機会があれば今後別のデータ拡張手法のカスタマイズについても紹介したいと思います。